动态创建 Script 标签
比如我们要加载 a.js,一般会这么写:
var head = document.getElementsByTagName('head')[0]
var script = document.createElement('script')
script.type = 'text/javascript'
script.src = 'a.js'
head.appendChild(script)
说一个知识点,后面会用到:
Opera 这货是个彻彻底底的两面派,比如它支持 IE 的 attachEvent,也支持标准的 addEventListener; 它支持 IE 的 currentStyle,也支持标准的 window.getComputedStyle;不一而足。
所以有时候专门针对 IE 的 fix,需要排除 Opera,因为它既然实现了更好的方式,我们就要用更好的,我们的目的是落后的 IE,仅此而已!
Opera 检测技巧:var isOpera = typeof opera !== 'undefined' && opera.toString() === '[object Opera]';
如果我们需要调用 a.js 里的 fn()方法,因为这个过程是异步的,所以要等到 js 文件加载完成时才能调到,怎么判断是否完成加载呢?
var isOpera =
typeof opera !== 'undefined' && opera.toString() === '[object Opera]'
head = document.getElementsByTagName('head')[0]
script = document.createElement('script')
script.type = 'text/javascript'
if (script.attachEvent && !isOpera) {
script.attachEvent('readystatechange', onScriptLoad)
} else {
script.addEventListener('load', onScriptLoad)
}
script.src = 'a.js'
head.appendChild(script)
悬念继续留给 onScriptLoad 方法,这里再插一个知识点:readyState,它包括以下值:
0: "uninitialized" – 原始状态
1: "loading" – 正在加载
2: "loaded" – 加载完成
3: "interactive" – 还未执行完毕
4: "complete" – 脚本执行完毕
为什么我写了 ":" 呢,因为在 xhr 中,请求完成时的 readyState 为数字形式,即 ":" 左侧的部分,而以节点加载时,readyState 为字符串形式,即 ":" 右侧的部分。其中涉及的兼容性问题,请参看PPK
为什么要说这个呢,当然是为了 IE 这厮。其他浏览器在脚本加载完成时,会发出 onload 事件,所以不存在问题,但是 IE 不知 onload 为何物,所以它独创一派。
经我测试 Chrome14, Firefox8, Opera11, Safari5, onload 事件触发的时机是在脚本执行完之后,比如请求 a.js,这个文件的最后一行写上 alert('xxx'); 然后 script.onload = function() { alert('onload'); },打印顺序一致是 xxx -> onload。
我又试了 script.addEventListener('load', function() { alert('onload'); },结果相同。
IE 支持 onreadystatechange 事件,Opera 则两个都支持,同样的还是要过滤掉 Opera。肯定有人会问,那 IE9 呢?好吧,我承认我不清楚,我只是听说 IE9 的 addEventListener 方法和别的标准浏览器表现不太一致,所以在这把 IE9 一并归入传统 IE 浏览器的范畴了。
怎么判断脚本是否加载完成呢?一般的做法是判断 script.readyState,IE 就是个变态,连这个值都不是固定的,所以需要这么做:
script.readyState === 'loaded' || script.readyState === 'complete'
关于这里的加载判断,我参考了好几个框架的设计,除了 RequireJS 多一句 event.type === 'load',大多都是用上面这段,所以咱也用这句,要死大家一起死吧。
还有一点,因为我们统一放在 onScriptLoad 里处理,而在标准浏览器中 script.readyState 为 undefined; 为了防止内存泄漏,最好在加载完成后把 script 节点移除,参看代码:
function onScriptLoad(e) {
e = e || window.event
var script = e.target || e.srcElement
if (/loaded|complete|undefined/.test(script.readyState)) {
if (script.detachEvent && !isOpera) {
script.detachEvent('onreadystatechange', onScriptLoad)
} else {
script.removeEventListener('load', onScriptLoad)
}
var head
if ((head = script.parentNode)) {
try {
if (script.clearAttribute) {
script.clearAttribute()
} else {
for (var prop in script) {
delete script[prop]
}
}
} catch (e) {}
head.removeChild(script)
}
}
}
再来看一个问题,现在有个需求,比如脚本 a 依赖 脚本 b,a 肯定要等到 b 加载并执行完之后才能开始执行,这怎么办呢?
\1. 串行加载,即一个加载完再加载下一个(较慢)
\2. 并行加载
第一种方法没什么可说的,这里说第二种。
先介绍一个属性:async
当 script 的 async 属性为 true 时,脚本的执行序为异步的。即不按照加入 DOM 的顺序执行;如果是 false 则按加入的顺序执行。
如果 script 标签被直接编码到 HTML 中,黙认的 async 属性为 false;如果 script 是由 document.createElement('script') 创建的,那么 async 属性为 true。
检测方法: var script = document.createElement('script'); script.async === true;
检测结果: IE6-9, Opera11, Safari5 不支持
再介绍一个属性:defer
defer 属性规定是否延迟执行脚本,直到页面加载为止,默认值为 false,具体情况可参考我最后给出的链接
触发方式:script.defer = 'defer'
检测方式:var script = document.createElement('script'); script.defer === false;
检测结果:所有浏览器都支持
相同点 | 不同点 |
---|---|
带有 async 或 defer 的 script 都会立刻下载,不阻塞页面解析,而且都提供一个可选的 onload 事件处理,在 script 下载完成后调用,用于做一些和此 script 相关的初始化工作。 | script 执行的时机不同。带有 async 的 script,一旦下载完成就开始执行(当然是在 window 的 onload 之前)。这意味着这些 script 可能不会按它们出现在页面中的顺序来执行,如果你的脚本互相依赖并和执行顺序相关,就有很大的可能出问题。而对于带有 defer 的 script,它们会确保按在页面中出现的顺序来执行,它们执行的时机是在页面解析完后,但在 DOMContentLoaded 事件之前。 |
---|---|
接着讲刚才的问题,我们关心的不是谁先加载,而是谁先执行,是执行顺序的问题,所以如果浏览器支持 async 属性,记得设置为 false,然后按你需要的顺序进行 appendChild 就行了,但是这种方式明显是不兼容的...
还好,我们还有 defer,浏览器都支持。
最后用 Script 方式 和 XHR 方式做个比较:
优点 | 缺点 |
---|---|
1. 具有跨域能力 2. 即使 ActiveX 被关了也可以在 IE 中运行 3. 可以在不支持 xhr 的老旧浏览器上运行 | 1. 返回的数据必须格式化为 js 代码,而 xhr 返回的数据可以是任何格式,XML, JSON, 纯文本等等 2. 只支持 GET 请求,不支持 POST3. 请求是异步还是同步完全取决于浏览器,而 xhr 可以由你控制 4. 当从一个不受信任的来源获取 JSON 数据时,你没办法在代码执行前检查这些数据,而 xhr 可以用一些工具分析数据,比如 json2.js |
---|---|
还有一个不同就是,动态创建 script 节点所加载的文件,如果在当前上下文调用 eval()来处理,那么该文件中定义的变量和函数都是全局的。如果你希望加载的数据只是局部可用,那就用 xhr 吧。
这两种方式各有各的好处,总的来说,如果是需要加载一段代码,最好使用 动态创建 script 节点 的方式,如果是请求数据,最好使用 xhr。